package com.szwistar.common.comm;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class PrefixedFrameDecoder extends CumulativeProtocolDecoder {
	static final Logger LOGCOMM = LoggerFactory.getLogger("comm");

	// 帧同步头标志
	byte[] prefix;
	// 帧长度指示字段大小(1/2/4)
	int lengthIndicatorSize;
	// 最小帧长度（不包括同步头标志，包括帧长度指示字节）
	int minFrameSize;
	// 最大帧长度（不包括同步头标志，包括帧长度指示字节）
	int maxFrameSize;

	/**
	 * 构造函数
	 * @param prefix 帧同步头标志
	 * @param lengthIndicatorSize 帧长度指示字段大小(1/2/4)
	 * @param minFrameSize 最小帧长度（不包括同步头标志，包括帧长度指示字节）
	 * @param maxFrameSize 最大帧长度（不包括同步头标志，包括帧长度指示字节）
	 */
	public PrefixedFrameDecoder(byte[] prefix, int lengthIndicatorSize, int minFrameSize, int maxFrameSize) {
		super();
		this.prefix = (prefix == null) ? (new byte[0]) : prefix;
		this.lengthIndicatorSize = lengthIndicatorSize;
		this.minFrameSize = minFrameSize;
		this.maxFrameSize = maxFrameSize;
	}

	/**
	 * 派生类应该实现此接口，解析自己的业务数据帧
	 * @param in 数据缓冲区，包含帧长度指示字节
	 * @param isFrameLengthSure 帧长度是否是确定的。
	 * 		如果是确定的，则派生类在解析时，不管此数据是否正确，都应该把数据处理掉，不能返回false（因为不可能有更多数据给它了）！
	 * 		如果是不确定的，如果派生类在解析时发现数据帧是变长帧，可以返回false，以要求尝试提供更多数据。
	 * @return 如果因为数据不足而无法解析成功，可以返回false继续等待；其它情形应该返回true
	 * @throws Exception
	 */
	public abstract boolean decodeFrame(IoSession session, IoBuffer in, boolean isFrameLengthSure, ProtocolDecoderOutput out) throws Exception;

	/**
	 * 解码
	 * @return <tt>true</tt>：当且仅当缓冲区中有更多数据需要继续解码<br>
	 *         <tt>false</tt>：当缓冲区中剩余的数据不够解码了，需要收集更多数据后再进行解码<br>
	 * {@inheritDoc}
	 */
	@Override
	protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
		//LOGCOMM.debug("\n\t[{}]", in.getHexDump());

		// 查找并过滤同步头前面的数据
		if(!seekPrefix(in)) { return false; }

		// 记住帧同步头开始位置
		int syncPos = in.position();
		// 跳过帧同步头
		in.skip(prefix.length);
		// 记住帧数据开始位置
		int framePos = in.position();
		// 获取帧长度指示
		int frameLen = peekLength(in);
		// 数据不足，需要继续收集数据
		if(frameLen < 0) {
			// 恢复帧头位置(包括帧头、帧长度指示)，并返回继续等待
			in.position(syncPos);
			return false;
		}

		//
		// Case: 很好！协议有帧长度指示
		//
		if(frameLen > 0) {
			frameLen += lengthIndicatorSize;

			if( (frameLen < minFrameSize) || (frameLen > maxFrameSize) ) {
				// 帧长度错误！
				LOGCOMM.error("数据帧长度指示 {} 不是合法值！尝试重新建立同步！", frameLen);
				// 返回true以便尝试重新建立同步
				return true;
			}
			else if(frameLen > in.remaining()) {
				//LOGCOMM.debug("依帧长度指示，数据不足: {}/{}", in.remaining(), frameLen);
				// 恢复帧头位置(包括帧头、帧长度指示)，并返回继续等待
				in.position(syncPos);
				return false;
			}

			try {
				// 数据长度足够，可以解析业务帧了
				// 给业务层解析使用此帧数据的切片/slice，以防破坏其它数据！
				IoBuffer fbuf = in.getSlice(frameLen);
				decodeFrame(session, fbuf, true, out);
			} catch (Exception e) {
				LOGCOMM.error("解析数据帧异常！{}", e.getMessage());
			}

			// 帧长度确定的情况下，不管解码是否正常，都要返回true以便开始下一帧处理
			return true;
		}

		//
		// Case: 协议没有帧长度指示，比较麻烦！
		//
		// 尝试再次搜索帧头，如果发现帧头，则可准确认定一个数据帧的范围
		if(seekPrefix(in)) {
			// 还好！找到了下一个帧头(当前位置就是下一个帧头位置)，可以计算出帧长度了
			int nextSyncPos = in.position();
			frameLen = nextSyncPos - framePos;
			// 设置指针位置指向本帧的开始位置
			in.position(framePos);
			// 给业务层解析使用此帧数据的切片/slice，以防破坏其它数据！
			IoBuffer fbuf = in.getSlice(frameLen);

			try {
				decodeFrame(session, fbuf, true, out);
			} catch (Exception e) {
				LOGCOMM.error("解析数据帧异常！", e);
			}

			// 帧长度确定的情况下，不管解码是否正常，都要返回true以便开始下一帧处理
			in.position(nextSyncPos);
			return true;
		}

		//
		// Case: 糟糕！没有下一个帧头，帧长度无法准确判断，只能试着解析一下看看了！
		//
		try {
			// 所有数据都给它，让它试着去解码吧！
			in.position(framePos);
			boolean ret = decodeFrame(session, in, false, out);
			if(ret) {
				// 解析OK！返回true以便开始下一帧处理
				return true;
			} else {
				// 数据不足
				// 恢复帧头位置(包括帧头、帧长度指示)，并返回继续等待
				in.position(syncPos);
				return false;
			}
		} catch (Exception e) {
			LOGCOMM.error("解析数据帧异常！缓冲区中数据状态无法判断，清空所有数据！{}", e.getMessage());
			in.clear();
			return false;
		}
	}

	/**
	 * 定位帧头，丢弃帧头前的数据<br><br>
	 * 返回时 mark: 不会改变<br>
	 * 返回时 position: 指向同步头开始的位置(true)或已建立同步的数据(false)<br>
	 * @param in
	 * @return true: 匹配成功，当前位置为帧头位置<br>
	 *         false: 匹配了所有数据，都尚未找到帧头，所有数据丢弃
	 */
	boolean seekPrefix(IoBuffer in) {
		int init = in.position();
		int match = 0;

		while(in.remaining() > 0) {
			if(in.get() == prefix[match]) {
				// 如果当前字节匹配，则继续匹配下一个字节
				match++;
				if(match >= prefix.length) { break; }
			} else {
				// 如果不匹配，则重新开始匹配
				match = 0;
			}
		}

		// 保留已匹配的数据在缓冲区中
		in.position(in.position() - match);
		// 计算丢弃的数据长度
		if(in.position()-init > 0) {
			int pos = in.position();
			int len = pos - init;
			in.position(init); LOGCOMM.debug("帧定界：丢弃头部 {} 字节无效数据！\n\t{}", len, in.getHexDump(len)); in.position(pos);
			//LOGCOMM.debug("帧定界：丢弃头部 {} 字节无效数据！", len);
		}

		// 返回是否匹配成功
		return (match >= prefix.length);
	}

	/**
	 * 根据帧长度指示取得帧长度<br><br>
	 * 返回时 mark: 不会改变<br>
	 * 返回时 position: 不会改变<br>
	 * @return >0: 帧长度指示<br>
	 *         =0: 协议无帧长度指示<br>
	 *         <0: 缓冲区中数据长度不足
	 */
	int peekLength(IoBuffer in) {
		if(in.remaining() < lengthIndicatorSize) {
			//LOGCOMM.debug("帧定界：缓冲区中数据不足（没有帧长度指示）");
			return -1;
		}

		int len = -1;
		int init = in.position();

		if(lengthIndicatorSize == 1) {
			len = in.get() & 0xff;
		} else if(lengthIndicatorSize == 2) {
			len = in.getShort() & 0xffff;
		} else if(lengthIndicatorSize == 4) {
			len = in.getInt() & 0xffffffff;
		} else {
			len = 0;
		}

		in.position(init);
		return len;
	}
}
